Skip to content

Python SDK: PydanticAI adapter - #131

Merged
prashar32 merged 1 commit into
mainfrom
feat/pydantic-ai-adapter
Jun 14, 2026
Merged

Python SDK: PydanticAI adapter#131
prashar32 merged 1 commit into
mainfrom
feat/pydantic-ai-adapter

Conversation

@prashar32

Copy link
Copy Markdown
Owner

Adds a PydanticAI adapter to the Python SDK so a PydanticAI agent can be bound to a governed run with no change to the agent beyond wiring the adapter:

from pydantic_ai import Agent
from riskkernel.adapters.pydantic_ai import govern

agent = Agent(govern("anthropic:claude-sonnet-4-5", run, gate_tools=True))
agent.run_sync("...")   # halts with BudgetExceeded when the budget is spent

How it works

I wrap the model with PydanticAI's WrapperModel (which forwards every model method to the wrapped model) and override only request/request_stream. One model request counts as one governed step, so the deterministic loop/time budget is enforced over the agent's outer loop and a halt surfaces as BudgetExceeded. With gate_tools=True, each tool call the model proposes is routed through the human-approval gate before the agent executes it (a denial raises ApprovalDenied).

This keeps the adapter on a single stable surface — the Model.request contract, which hasn't changed across the post-1.0 line — for both loop/time enforcement and tool gating, rather than depending on a newer hooks API. The proposed tool calls are present in the model's response before the agent runs them, so I gate them right after the response comes back and before they execute.

Propagation

PydanticAI only retries a model request on its own ModelRetry signal and re-raises every other model-request error by default, so I deliberately raise plain BudgetExceeded/ApprovalDenied (never ModelRetry): they propagate out of agent.run()/run_sync() and stop the agent rather than being retried into another paid request. This matches the propagation discipline of the LangChain (raise_error) and CrewAI (step_callback vs event bus) adapters.

pydantic-ai is lazily imported and added only as an optional extra (pip install riskkernel[pydantic-ai]), so the core SDK still installs and imports with no third-party dependencies. Supported against pydantic-ai (pydantic-ai-slim) >= 1, < 2.

Tests

tests/test_pydantic_ai.py — stdlib-only unit tests (step ticking, loop-budget halt surfaced-not-swallowed with the underlying request skipped, tool gating off/on/multi-call/final-answer/denied) plus two skipUnless(pydantic-ai installed) integration tests that drive a real Agent with a FunctionModel and prove a runaway agent halts at its loop budget and a denied tool blocks. Ran the full SDK suite (passes; pydantic-ai-gated tests skip without it) and verified the integration tests against a real pydantic-ai 1.107.0 install in a throwaway venv — all green.

Closes #85

Bind a PydanticAI agent to a governed run by wrapping its model:
`Agent(govern(model, run))` — no other change to the agent. One model
request counts as one governed step, so the deterministic loop/time
budget halts a runaway agent, and with `gate_tools=True` each tool call
the model proposes routes through the human-approval gate before the
agent executes it.

I built this on PydanticAI's `WrapperModel`, which forwards every model
method to the wrapped model, so I only override `request`/`request_stream`.
That keeps it to one stable surface (the `Model.request` contract, which
hasn't changed across the post-1.0 line) and covers both loop/time
enforcement and tool gating: the tool calls a step will make are present
in the model's response before the agent runs them, so I gate them right
after the response comes back.

The halt propagates: PydanticAI only retries on its own `ModelRetry`
signal and re-raises every other model-request error by default, so I
raise plain `BudgetExceeded`/`ApprovalDenied` (never `ModelRetry`) and
they bubble out of `agent.run()`/`run_sync()` and stop the agent rather
than being retried into another paid request. I verified this against a
real pydantic-ai install with a FunctionModel that loops forever.

`pydantic-ai` is lazily imported and stays an optional extra, so the SDK
still installs and imports with no third-party deps. Supported against
pydantic-ai (pydantic-ai-slim) >= 1, < 2.
@prashar32
prashar32 force-pushed the feat/pydantic-ai-adapter branch from 09bfeff to f467136 Compare June 14, 2026 16:10
@prashar32
prashar32 merged commit f78381b into main Jun 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PydanticAI adapter

1 participant